import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import rcParams
import matplotlib.font_manager as fm
from sklearn.preprocessing import StandardScaler

# 步骤 1：配置Matplotlib以支持中文字体显示及解决负号显示异常问题
# 首先指定SimHei字体的路径，此路径是Windows系统下的默认路径，若在其他系统运行需相应调整
font_path = 'C:/Windows/Fonts/simhei.ttf'
# 通过字体路径创建字体属性对象，后续用于设置Matplotlib的字体
prop = fm.FontProperties(fname=font_path)
# 将Matplotlib全局的字体族设置为SimHei，确保在绘图时能够正确显示中文
rcParams['font.family'] = prop.get_name()
# 解决在坐标轴等地方负号可能显示为方块的问题，设置为False使其正常显示
rcParams['axes.unicode_minus'] = False

# 步骤 2：加载数据集
# 从UCI机器学习数据库指定的URL获取葡萄酒数据集，该数据集常用于数据分析与机器学习实践
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data"
# 定义数据集各列的名称，其中包含表示葡萄酒类别的列以及多个描述葡萄酒化学特性的特征列
columns = ['Class', 'Alcohol', 'Malic_acid', 'Ash', 'Alcalinity_of_ash', 'Magnesium',
           'Total_phenols', 'Flavanoids', 'Nonflavanoid_phenols', 'Proanthocyanins',
           'Color_intensity', 'Hue', 'OD280/OD315', 'Proline']
# 使用pandas库的read_csv函数读取数据集，由于原始数据文件没有标题行，所以设置header=None，
# 并通过names参数按照定义好的列名来解析数据，最终将数据加载为DataFrame格式
data = pd.read_csv(url, header=None, names=columns)

# 步骤 3：数据预处理 - 筛选特定类别数据
# 从原始数据中筛选出类别为1和2的数据子集，这样做可以聚焦于部分数据进行后续分析，简化问题复杂度
filtered_data = data[data['Class'].isin([1, 2])]
# 提取筛选后数据中的特征部分，即除了表示类别的第一列之外的所有列，作为后续聚类分析的输入特征数据
X = filtered_data.iloc[:, 1:]

# 步骤 4：特征数据标准化
# 创建StandardScaler对象，它会依据数据的均值和标准差对数据进行标准化处理，
# 使得数据的特征具有相同的尺度，有助于提升聚类算法的性能和收敛速度
scaler = StandardScaler()
# 对提取的特征数据进行拟合（学习数据的均值和标准差）并转换（将数据标准化）操作，得到标准化后的特征数据
X_scaled = scaler.fit_transform(X)

# 步骤 5：实现K-Means聚类算法
def kmeans(X, k=2, max_iters=100):
    """
    K-Means聚类算法的实现函数。

    参数：
    X (numpy.ndarray)：输入的特征数据，形状为 (样本数量, 特征数量)，表示待聚类的样本及其特征信息。
    k (int, 可选)：期望将数据聚成的类别数量，默认值为2，表示将数据划分为两个簇，可根据实际需求调整。
    max_iters (int, 可选)：算法的最大迭代次数，用于控制算法的运行时长，避免陷入无限循环，默认值为100。

    返回：
    centroids (numpy.ndarray)：最终得到的聚类中心坐标，形状为 (k, 特征数量)，每个元素对应一个聚类中心在各特征维度上的坐标值。
    labels (numpy.ndarray)：每个样本所属聚类的标签，形状为 (样本数量,)，其元素值为对应样本所属聚类的编号（从0到k - 1）。
    """
    # 随机从输入的特征数据中选择k个样本点作为初始的聚类中心，确保每个聚类中心都是唯一的（replace=False），
    # 这样可以为聚类算法提供不同的起始状态，避免每次运行结果都相同（因为初始值不同可能导致最终聚类结果有差异）
    centroids = X[np.random.choice(X.shape[0], k, replace=False)]
    prev_centroids = np.zeros_like(centroids)
    labels = np.zeros(X.shape[0])

    # 开始迭代过程，不断更新聚类中心和样本所属的聚类标签，直到满足停止条件（聚类中心不再变化或达到最大迭代次数）
    for _ in range(max_iters):
        # 步骤 6：为每个样本点分配最近的聚类中心（归类步骤）
        for i in range(X.shape[0]):
            # 计算当前样本点与各个聚类中心之间的欧氏距离，axis=1表示沿着第二个维度（即每个聚类中心维度）计算距离
            distances = np.linalg.norm(X[i] - centroids, axis=1)
            # 根据计算得到的距离，找到距离当前样本点最近的聚类中心，将该聚类中心的编号作为样本点的聚类标签
            labels[i] = np.argmin(distances)

        # 步骤 7：更新聚类中心（重新计算每个聚类的中心位置）
        for i in range(k):
            # 根据样本点的聚类标签，筛选出属于当前聚类的所有样本，然后计算这些样本在各个特征维度上的平均值，
            # 以此更新当前聚类的中心位置，使其更能代表所属聚类内样本的特征分布
            centroids[i] = X[labels == i].mean(axis=0)

        # 检查当前的聚类中心与上一轮迭代得到的聚类中心是否完全相同，如果所有聚类中心都没有变化，说明算法已经收敛，
        # 此时可以提前停止迭代，节省计算资源
        if np.all(centroids == prev_centroids):
            break
        prev_centroids = centroids.copy()

    return centroids, labels

# 步骤 6：运行K-Means聚类算法
# 调用kmeans函数，传入标准化后的特征数据X_scaled，并指定聚类类别数k为2，执行聚类操作，得到聚类中心和样本标签
centroids, labels = kmeans(X_scaled, k=2)

# 步骤 7：可视化聚类结果
plt.figure(figsize=(8, 6))
# 绘制散点图展示聚类结果，将标准化后的特征数据的前两个维度作为横纵坐标（通常可以直观展示二维平面上的聚类分布情况），
# 根据样本所属的聚类标签为每个样本点设置颜色（通过cmap='viridis'指定颜色映射），设置边缘颜色为黑色，点的大小为50
plt.scatter(X_scaled[:, 0], X_scaled[:, 1], c=labels, cmap='viridis', edgecolor='k', s=50)
# 在图中绘制聚类中心，以红色的'X'标记显示，设置标记大小为200，并添加图例说明其为聚类中心
plt.scatter(centroids[:, 0], centroids[:, 1], c='red', marker='X', s=200, label='聚类中心')
# 为图表添加标题，清晰表明图表展示的是K-Means聚类结果
plt.title('K-Means 聚类结果')
# 设置x轴标签，说明横坐标代表酒精含量（Alcohol）这一特征
plt.xlabel('酒精含量 (Alcohol)')
# 设置y轴标签，说明纵坐标代表苹果酸含量（Malic acid）这一特征
plt.ylabel('苹果酸含量 (Malic acid)')
# 显示图例，方便查看图表时理解不同元素所代表的含义
plt.legend()
# 显示绘制好的图表，呈现聚类结果的可视化图像
plt.show()